﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

namespace GnuPGNotepad
{
    public partial class frmMain : Form
    {
        #region public declarations
        public Settings Config = new Settings();
        public const string RecipientTemplate = "Recipient(s): {0}";
        public string TitleTemplate = "{0} - " + Application.ProductName + " v" + Application.ProductVersion;
        public Regex PGPMsgRegex = new Regex("-----BEGIN PGP MESSAGE-----(.*?)-----END PGP MESSAGE-----", RegexOptions.Singleline);
        #endregion // public declarations

        #region private declarations
        private Stream outputStream = null;
        private string exitMessage = null;
        #endregion // private declarations

        public frmMain()
        {
            InitializeComponent();

            // initialize textbox default settings
            initializeNewTextBox();            

            // set tooltip with binary path
            setBinaryPathToolStripMenuItem.ToolTipText = Config.GPGBinaryPath;

            // update status bar for recipients if loaded from user config
            updateRecipientList();

        }

        private void initiateAction(PGPAction action, string input)
        {
            if (string.IsNullOrEmpty(input))
                return;

            // check if content has PGP Message Header
            bool pgpMessageHeader = PGPMsgRegex.IsMatch(input);

            switch (action)
            {
                case PGPAction.Encrypt:
                case PGPAction.SignAndEncrypt:
                case PGPAction.Sign:
                    if (pgpMessageHeader)
                    {
                        Utils.ShowMessage("The content already appears to be encrypted", MessageBoxIcon.Warning);
                        return;
                    }                
                    break;
                case PGPAction.Decrypt:
                case PGPAction.Verify:
                    if (!pgpMessageHeader)
                    {
                        Utils.ShowMessage("There does not appear to be any content to decrypted", MessageBoxIcon.Warning);
                        return;
                    }
                    break;
                default:
                        Utils.ShowMessage("Unknown action was selected", MessageBoxIcon.Error);
                        return;
            }

            try
            {
                using (MemoryStream inputStream = (MemoryStream)Utils.ConvertTo(input))
                {
                    // send info to be executed - result code indicates success or not
                    int exitCode = Execute(action, inputStream);

                    // send exit code and output stream (as text) to display results
                    displayResults(exitCode, Utils.ConvertTo(outputStream));
                }
                // exceptions are handled in the Execute function
            }
            finally
            { 
                if(null != outputStream)
                    outputStream.Dispose(); 
            }
        }

        private int Execute(PGPAction action, Stream inputStream)
        {
            // reset global objects
            exitMessage = null;
            outputStream = new MemoryStream();
            MemoryStream tempStream = new MemoryStream();
            int exitCode = -1;

            try
            {
                // execute syntax
                using (App app = new App())
                {
                    app.Action = action;
                    app.Arguments = getArguments(action);

                    app.Run(inputStream, tempStream);

                    // capture exit info
                    exitCode = app.ExitCode;
                    exitMessage = app.ExitMessage;

                    // set position to beginning of stream
                    tempStream.Position = 0;

                    // read output and load results into outputStream object
                    Utils.WriteTo(tempStream, outputStream);

                    // return exit code of execution
                    return exitCode;
                }
            }
            catch (Exception ex)
            { 
                Utils.ShowMessage(ex.Message, MessageBoxIcon.Error);
                return -1;
            }
            finally
            {
                if (null != tempStream)
                    tempStream.Dispose();
            }       
        }

        private string getArguments(PGPAction Action)
        {
            // initialize arguments string builder
            StringBuilder args = new StringBuilder("--no-verbose --batch --trust-model always --default-recipient-self ");

            // load home directory if provided
            if (!string.IsNullOrEmpty(Config.HomeDirectory))
                args.AppendFormat("--homedir \"{0}\" ", Config.HomeDirectory);

            // if populated, add all recipients in recipients object
            if(Config.Recipients.Count > 0)
                Config.Recipients.ForEach(delegate(string recipient)
                { 
                    args.Append(string.Format("--recipient \"{0}\" ", recipient)); 
                });

            // add armor if binary not selected
            if (!Config.SaveBinaryFormat)
                args.Append("--armor ");

            switch (Action)
            {
                case PGPAction.Decrypt:
                    args.Append("--decrypt");
                    break;
                case PGPAction.Sign:
                    args.Append("--sign");
                    break;
                case PGPAction.Verify:
                    args.Append("--verify");
                    break;
                case PGPAction.Encrypt:
                    args.Append("--encrypt");
                    break;
                case PGPAction.SignAndEncrypt:
                default:
                    args.Append("--sign --encrypt");
                    break;
            }

            // return finalalized argument string
            return args.ToString();
        }

        private bool openFile()
        {
            StreamReader contentStream = null;

            // check if there are unsaved changes to be committed
            if (!changesCommitted())
                return false;

            try
            {
                string fileName = string.Empty;

                // prompt user for filename
                using (OpenFileDialog ofd = new OpenFileDialog())
                {
                    ofd.InitialDirectory = Config.ActiveFilePath;
                    ofd.RestoreDirectory = true;
                    ofd.Title = Application.ProductName;
                    ofd.DefaultExt = "asc";
                    ofd.Filter = "asc files (*.asc)|*.asc|Text Files (*.txt)|*.txt|All files (*.*)|*.*";
                    ofd.CheckFileExists = true;
                    ofd.CheckPathExists = true;
                    ofd.Multiselect = false;

                    // user canceled dialog
                    if (ofd.ShowDialog() != DialogResult.OK)
                        return false;

                    fileName = ofd.FileName;
                }

                // make sure the file actually exists
                if (!File.Exists(fileName))
                    throw new FileNotFoundException("Unable to locate the file", fileName);

                // bind to file
                FileInfo file = new FileInfo(fileName);

                // load file into stream
                contentStream = new StreamReader(file.ToString());

                if (null == contentStream || !contentStream.BaseStream.CanRead)
                    throw new FileLoadException("Unable to load file content", file.Name);

                // decrypt content
                int exitCode = Execute(PGPAction.Decrypt, contentStream.BaseStream);

                // if exitcode is -1 then something bad happened and user
                // was already notified. just exit with false return value
                if (exitCode == -1)
                    return false;

                // if exit code was anything other than 0 (and not -1) then
                // something else happened (who knows what) and the save didn't
                // work... send to display results to user
                if (exitCode != 0)
                {
                    displayResults(exitCode, Utils.ConvertTo(outputStream));
                    return false;
                }

                string content = null;
                // if outputstream is readable then load into content as text
                if (outputStream.CanRead)
                    content = Utils.ConvertTo(outputStream);

                // check if content is empty
                if (string.IsNullOrEmpty(content))
                {
                    // notify user of a problem
                    Utils.ShowMessage("Unable to read the file contents", MessageBoxIcon.Warning);
                    return false;
                }

                // now that file is loaded, let's set the ActiveFile name
                Config.ActiveFile = file.ToString();

                // populate textbox with content
                tbInput.Text = content;

                // update the form title with file name
                this.Text = string.Format(TitleTemplate, Config.ActiveFileName);

                // all done
                return true;
            }
            catch (Exception ex)
            {
                Utils.ShowMessage(ex.Message, MessageBoxIcon.Error);
                return false;
            }
            finally
            {
                if (null != outputStream)
                    outputStream.Dispose();

                if (null != contentStream)
                    contentStream.Dispose();
            }
        }

        private void newFile()
        {
            // if contents have been modified ask to save
            if (tbInput.Modified)
            {
                // if unable to commit changes then exit without clearing out file
                if (!changesCommitted())
                    return; 
            }

            // initialize a new textbox
            initializeNewTextBox();
        }

        private bool saveAs()
        {
            using (SaveFileDialog sfd = new SaveFileDialog())
            {
                sfd.InitialDirectory = Config.ActiveFilePath;
                sfd.RestoreDirectory = true;
                sfd.Title = "Save to an encrypted file";
                sfd.DefaultExt = "asc";
                sfd.Filter = "asc files (*.asc)|*.asc|Text Files (*.txt)|*.txt|All files (*.*)|*.*";
                sfd.CheckPathExists = true;

                // clicked OK, send to be saved
                if (sfd.ShowDialog() == DialogResult.OK)
                    return saveFile(sfd.FileName);

                // clicked cancel, exit
                return false;
            }
        }

        /// <summary>
        /// Saves the input content to the specified filename
        /// </summary>
        /// <param name="fileName">(Optional)If filename not provided then call will be made to saveAs</param>
        /// <returns>True/False of save action</returns>
        private bool saveFile(string fileName)
        {
            if (string.IsNullOrEmpty(tbInput.Text))
                return false;

            // Needed if content is already encrypted
            bool encryptContent = true;

            // load input text
            MemoryStream contentStream = (MemoryStream)Utils.ConvertTo(tbInput.Text);

            try
            {
                // if filename is empty then send to saveAs function - looping back here with a filename
                if (string.IsNullOrEmpty(fileName))
                    return saveAs(); 

                // check if the content is already encrypted
                if (PGPMsgRegex.IsMatch(tbInput.Text))
                {
                    // content appears to be encrypted already.  we dont want to encrypt on top
                    DialogResult dialogResult = 
                        MessageBox.Show("The content appears to be encrypted.  Continue saving without additional encryption?", 
                            Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question);

                    // return bool based on what the user wants to do
                    if (dialogResult == DialogResult.Yes)
                        encryptContent = false;
                    else
                        return false;
                }

                // if required, encrypt content
                if (encryptContent)
                {
                    int exitCode = Execute(PGPAction.Encrypt, contentStream);

                    // if exitcode is -1 then something bad happened and user
                    // was already notified. just exit with false return value
                    if (exitCode == -1)
                        return false;

                    // if exit code was anything other than 0 (and not -1) then
                    // something else happened (who knows what) and the save didn't
                    // work... send to display results to user
                    if (exitCode != 0)
                    {
                        displayResults(exitCode, Utils.ConvertTo(outputStream));
                        return false;
                    }
                }
                else
                {
                    // initialize outputStream
                    outputStream = new MemoryStream();

                    // load content stream directly into outputStream
                    Utils.WriteTo(contentStream, outputStream);
                }

                // if outputstream is readable then proceed onward
                if (outputStream.CanRead)
                {
                    // set outputstream to beginning
                    outputStream.Position = 0;

                    // initialize new file for write
                    using (FileStream fs = File.Open(fileName, FileMode.Create, FileAccess.Write))
                    {
                        // copy output stream into filestream
                        Utils.WriteTo(outputStream, fs);

                        // write buffer to disk
                        fs.Flush();
                    }

                    // safe file name into memory
                    Config.ActiveFile = fileName;

                    // reset textbox modified value
                    tbInput.Modified = false;

                    // set the form title
                    this.Text = string.Format(TitleTemplate, Config.ActiveFileName);

                    // finished saving file
                    Utils.ShowMessage(string.Format("Successfully saved {0}", fileName), MessageBoxIcon.Information);
                 
                    return true;
                }
                else 
                {
                    Utils.ShowMessage("An unexpected error occurred while saving the file", MessageBoxIcon.Error);
                    return false;
                }
            }
            catch (Exception ex)
            {
                Utils.ShowMessage(ex.Message, MessageBoxIcon.Error);
                return false;
            }
            finally
            {
                if (null != outputStream)
                    outputStream.Dispose();

                if (null != contentStream)
                    contentStream.Dispose();
            }
        }

        /// <summary>
        /// displays the results of the execution
        /// </summary>
        /// <param name="exitCode">the exit value provided by the GnuPG binary</param>
        /// <param name="content">the text to be displayed</param>
        private void displayResults(int exitCode, string content)
        {
            if (exitCode != 0)
            {
                // if exit code isn't 0 then display the exitMessage
                Utils.ShowMessage(exitMessage, string.Format("Application Error", TitleTemplate), MessageBoxIcon.Warning);
                return;
            }

            // display any info the console provided - typical of validate and such
            if (!string.IsNullOrEmpty(exitMessage))
                Utils.ShowMessage(exitMessage, MessageBoxIcon.Information);

            // if content is empty then don't clear out previous info, allowing
            // the user to try again with existing content
            if (string.IsNullOrEmpty(content))
                return;

            // check if to content in primary field or output panel
            if (showOutputToolStripMenuItem.Checked)
                tbOutput.Text = content;
            else
                tbInput.Text = content;
        }

        /// <summary>
        /// sets the input textbox and application details to new state
        /// </summary>
        private void initializeNewTextBox()
        {
            // clear any existing content
            tbInput.Text = string.Empty;

            // set the form title to notify the user
            this.Text = string.Format(TitleTemplate, "Untitled", Application.ProductName);
        }

        /// <summary>
        /// adds entries into the settings recipients array - each recipient
        /// will be added to the command line for inclusion into the encryption/signing
        /// </summary>
        private void addRecipients()
        {
            // initialize a new recipients list
            List<string> recipients = null;
            string response = null;

            // continue here if the menu us unchecked, otherwise clear out previous recipients
            if (!setRecipientToolStripMenuItem.Checked)
            {
                // prompt user for list of recipients to add
                Utils.ShowGenericForm("Recipients", "Enter recipients separated by comma", Config.Recipient, false, out response);

                // check if anything was passed back
                if (!string.IsNullOrEmpty(response))
                {
                    // split response into array
                    recipients = new List<string>(response.Split(','));

                    // loop through and validate all recipients are in the key store
                    for (int i = 0; i < recipients.Count; i++)
                    {
                        string responseRaw = null;
                        // if not found in key store then notify user and remove from list
                        if (!GPGKeys.UIDExists(recipients[i].Trim(), out responseRaw))
                        {
                            // notify user unable to add to list
                            Utils.ShowMessage(string.Format("Unable to locate the public key for {0} in your keyring", recipients[i]), MessageBoxIcon.Warning);

                            // remove from recipients list
                            recipients.RemoveAt(i--);
                        }
                    }
                    
                    // set public variable if recipients has any entries
                    Config.Recipients = (recipients.Count == 0) ? null : recipients;
                }
            }
            else
            { 
                // user has unchecked the add recipients menu - assuming
                // the user wants to remove all entries and go back to default recipient
                Config.Recipients = null;
            }

            // if Recipients setting is popultated then check the option in toolstrip
            setRecipientToolStripMenuItem.Checked = (null != Config.Recipients);

            // update status bar to notify the user
            updateRecipientList();
        }

        /// <summary>
        /// update the status bar with the recipients list date from settings
        /// </summary>
        private void updateRecipientList()
        {
            // sets the default entry
            string recipientList = "Default-recipient-self";

            // if recipients in the settings then let's add them to array
            if (Config.Recipients.Count > 0)
                recipientList = string.Join(",", Config.Recipients.ToArray());

            // update status bar to notify the user
            tssRecipient.Text = string.Format(RecipientTemplate, recipientList);
        }

        /// <summary>
        /// Allows the user to overwrite the default paths of the GPG binary file
        /// </summary>
        private void updateBinaryPath()
        {
            string binaryPath = null;
            Utils.ShowGenericForm("GPG Binary Path", "Specify the path to the GnuPG binaries", Config.GPGBinaryPath, false, out binaryPath);

            // make sure the user provided a path
            if (!string.IsNullOrEmpty(binaryPath))
            {
                // make sure path specified exists
                if (!File.Exists(binaryPath)) 
                {
                    // if not found then don't update
                    Utils.ShowMessage("Unable to locate the provided path for the GnuPG binary", MessageBoxIcon.Warning);
                    return; 
                }

                // set binary path
                Config.GPGBinaryPath = binaryPath;

                // update the tooltip to show new path
                setBinaryPathToolStripMenuItem.ToolTipText = Config.GPGBinaryPath;
            }
        }

        /// <summary>
        /// Checks to see if there are any unsaved changes to the textbox and
        /// gives the user a chance to save the changes or ignore.
        /// </summary>
        /// <returns>True/False on whether changes have been addressed</returns>
        private bool changesCommitted()
        {
            DialogResult dialogResult;

            // no changes then return true immediately
            if (!tbInput.Modified)
                return true;

            // check if the content is already encrypted
            if (PGPMsgRegex.IsMatch(tbInput.Text))
            { 
                // content appears to be encrypted already.  we dont want to encrypt
                // the content again, so let's notify the user and ask what to do
                dialogResult = MessageBox.Show("You have unsaved changes, however the content appears to already be encrypted.  Would you like to contiue without saving the content?", Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                
                // return bool based on what the user wants to do
                return (dialogResult == DialogResult.Yes);
            }

            // ask to save currently modified contents
            dialogResult = MessageBox.Show("You have unsaved changes, would you like to save them now?", Application.ProductName, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);

            
            // user wants to think about it some more
            if (dialogResult == DialogResult.Cancel)
                return false;

            // user wants to save contents
            if (dialogResult == DialogResult.Yes)
            {
                // if new file then send to SaveAs to get filename and return result
                // as result of changesCommitted
                if (null == Config.ActiveFile)
                    return saveAs();

                // existing filename found, send to saveFile and return result
                return saveFile(Config.ActiveFile);
            }

            // user doesn't want to save contents, so return true
            return true;
        }

        /// <summary>
        /// show developer information.
        /// </summary>
        private void showAbout()
        {
            using (frmAbout fm = new frmAbout())
                fm.ShowDialog();
        }

        /// <summary>
        /// controls whether to display the second panel in the container
        /// </summary>
        private void showOutputPanel()
        {
            showOutputToolStripMenuItem.Checked = !showOutputToolStripMenuItem.Checked;
            splitContainer1.Panel2Collapsed = !showOutputToolStripMenuItem.Checked;
        }

        #region toolstripevents
        private void newFileToolStripMenuItem_Click(object sender, EventArgs e)
        {
            newFile();
        }
        private void openToolStripMenuItem_Click(object sender, EventArgs e)
        {
            openFile();
        }
        private void saveToolStripMenuItem_Click(object sender, EventArgs e)
        {
            saveFile(Config.ActiveFile);
        }
        private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
        {
            saveAs();
        }
        private void setRecipientToolStripMenuItem_Click(object sender, EventArgs e)
        {
            addRecipients();
        }
        private void saveBinaryToolStripMenuItem_Click(object sender, EventArgs e)
        {
            saveBinaryToolStripMenuItem.Checked = !saveBinaryToolStripMenuItem.Checked;
        }
        private void encryptToolStripMenuItem_Click(object sender, EventArgs e)
        {
            initiateAction(PGPAction.Encrypt, tbInput.Text);
        }
        private void decryptToolStripMenuItem_Click(object sender, EventArgs e)
        {
            initiateAction(PGPAction.Decrypt, tbInput.Text);
        }
        private void signAndEncryptToolStripMenuItem_Click(object sender, EventArgs e)
        {
            initiateAction(PGPAction.SignAndEncrypt, tbInput.Text);
        }
        private void verifyToolStripMenuItem_Click(object sender, EventArgs e)
        {
            initiateAction(PGPAction.Verify, tbInput.Text);
        }
        private void signToolStripMenuItem_Click(object sender, EventArgs e)
        {
            initiateAction(PGPAction.Sign, tbInput.Text);
        }
        private void setBinaryPathToolStripMenuItem_Click(object sender, EventArgs e)
        {
            updateBinaryPath();
        }
        private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
        {
            showAbout();
        }
        private void showOutputToolStripMenuItem_Click(object sender, EventArgs e)
        {
            showOutputPanel();
        }
        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            this.Close();
        }
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = !changesCommitted();
        }
        #endregion // toolstripevents

    }
}
